/* vi:set et sw=2 sts=2 cin cino=t0,f0,(0,{s,>2s,n-s,^-s,e-s:
 * Copyright © 2014 Red Hat, Inc
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.	 See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library. If not, see <http://www.gnu.org/licenses/>.
 *
 * Authors:
 *       Alexander Larsson <alexl@redhat.com>
 */

#include "config.h"

#include <locale.h>
#include <stdlib.h>
#include <string.h>
#include <gio/gio.h>
#include <glib/gprintf.h>
#include <polkit/polkit.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <pwd.h>
#include <gio/gunixfdlist.h>
#include <sys/mount.h>
#include <fcntl.h>
#include <unistd.h>

#include <glib-unix.h>

#include "flatpak-dbus-generated.h"
#include "flatpak-dir-private.h"
#include "flatpak-error.h"
#include "flatpak-image-source-private.h"
#include "flatpak-oci-registry-private.h"
#include "flatpak-progress-private.h"
#include "flatpak-system-helper.h"
#include "flatpak-utils-base-private.h"
#include "flatpak-utils-private.h"

static PolkitAuthority *authority = NULL;
static FlatpakSystemHelper *helper = NULL;
static GMainLoop *main_loop = NULL;
static guint name_owner_id = 0;

G_LOCK_DEFINE (cache_dirs_in_use);
static GHashTable *cache_dirs_in_use = NULL;

static gboolean on_session_bus = FALSE;
static gboolean disable_revokefs = FALSE;
static gboolean no_idle_exit = FALSE;

static int opt_verbose;
static gboolean opt_ostree_verbose;

#define IDLE_TIMEOUT_SECS 10 * 60

/* This uses a weird Auto prefix to avoid conflicts with later added polkit types.
 */
typedef PolkitAuthorizationResult AutoPolkitAuthorizationResult;
typedef PolkitDetails             AutoPolkitDetails;
typedef PolkitSubject             AutoPolkitSubject;

G_DEFINE_AUTOPTR_CLEANUP_FUNC (AutoPolkitAuthorizationResult, g_object_unref)
G_DEFINE_AUTOPTR_CLEANUP_FUNC (AutoPolkitDetails, g_object_unref)
G_DEFINE_AUTOPTR_CLEANUP_FUNC (AutoPolkitSubject, g_object_unref)

typedef struct
{
  FlatpakSystemHelper *object;
  GDBusMethodInvocation *invocation;
  GCancellable *cancellable;
  gboolean preserve_pull;     /* Whether to preserve partially pulled repo on pull failure */

  guint watch_id;
  uid_t uid;                  /* uid of the client initiating the pull */

  gint client_socket;         /* fd that is send back to the client for spawning revoke-fuse */
  gint backend_exit_socket;   /* write end of a pipe which helps terminating revokefs backend if
                                 system helper exits abruptly */

  gchar *src_dir;             /* source directory containing the actual child repo */
  gchar *unique_name;

  GSubprocess *revokefs_backend;
} OngoingPull;

static void
terminate_revokefs_backend (OngoingPull *pull)
{
  /* Terminating will guarantee that all access to write operations are revoked. */
  if (shutdown (pull->client_socket, SHUT_RDWR) == -1 ||
      !g_subprocess_wait (pull->revokefs_backend, NULL, NULL))
    {
      g_warning ("Failed to shutdown client socket, killing backend writer process");
      g_subprocess_force_exit (pull->revokefs_backend);
    }

  g_clear_object (&pull->revokefs_backend);
}

static gboolean
remove_dir_from_cache_dirs_in_use (const char *src_dir)
{
  gboolean res;

  G_LOCK (cache_dirs_in_use);
  res = g_hash_table_remove (cache_dirs_in_use, src_dir);
  G_UNLOCK (cache_dirs_in_use);

  return res;
}

static void
ongoing_pull_free (OngoingPull *pull)
{
  g_autoptr(GFile) src_dir_file = NULL;
  g_autoptr(GError) local_error = NULL;

  g_clear_handle_id (&pull->watch_id, g_bus_unwatch_name);

  src_dir_file = g_file_new_for_path (pull->src_dir);

  if (pull->revokefs_backend)
    terminate_revokefs_backend (pull);

  if (!pull->preserve_pull &&
      !flatpak_rm_rf (src_dir_file, NULL, &local_error))
    {
      g_warning ("Unable to remove ongoing pull's src dir at %s: %s",
                 pull->src_dir, local_error->message);
      g_clear_error (&local_error);
    }

  remove_dir_from_cache_dirs_in_use (pull->src_dir);

  g_clear_pointer (&pull->src_dir, g_free);
  g_clear_pointer (&pull->unique_name, g_free);
  close (pull->client_socket);
  close (pull->backend_exit_socket);

  g_clear_object (&pull->cancellable);
  g_slice_free (OngoingPull, pull);
}

G_DEFINE_AUTOPTR_CLEANUP_FUNC (OngoingPull, ongoing_pull_free);

static void
skeleton_died_cb (gpointer data)
{
  g_info ("skeleton finalized, exiting");
  g_main_loop_quit (main_loop);
}

static gboolean
unref_skeleton_in_timeout_cb (gpointer user_data)
{
  static gboolean unreffed = FALSE;

  g_info ("unreffing helper main ref");
  if (!unreffed)
    {
      g_object_unref (helper);
      unreffed = TRUE;
    }

  return G_SOURCE_REMOVE;
}

static void
unref_skeleton_in_timeout (void)
{
  if (name_owner_id)
    g_bus_unown_name (name_owner_id);
  name_owner_id = 0;

  /* After we've lost the name or idled we drop the main ref on the helper
     so that we'll exit when it drops to zero. However, if there are
     outstanding calls these will keep the refcount up during the
     execution of them. We do the unref on a timeout to make sure
     we're completely draining the queue of (stale) requests. */
  g_timeout_add (500, unref_skeleton_in_timeout_cb, NULL);
}

static gboolean
idle_timeout_cb (gpointer user_data)
{
  G_LOCK (cache_dirs_in_use);
  guint ongoing_pulls_len = g_hash_table_size (cache_dirs_in_use);
  G_UNLOCK (cache_dirs_in_use);
  if (ongoing_pulls_len != 0)
    return G_SOURCE_CONTINUE;

  if (name_owner_id)
    {
      g_info ("Idle - unowning name");
      unref_skeleton_in_timeout ();
    }
  return G_SOURCE_REMOVE;
}

G_LOCK_DEFINE_STATIC (idle);
static void
schedule_idle_callback (void)
{
  static guint idle_timeout_id = 0;

  G_LOCK (idle);

  if (!no_idle_exit)
    {
      if (idle_timeout_id != 0)
        g_source_remove (idle_timeout_id);

      idle_timeout_id = g_timeout_add_seconds (IDLE_TIMEOUT_SECS, idle_timeout_cb, NULL);
    }

  G_UNLOCK (idle);
}

static FlatpakDir *
dir_get_system (const char             *installation,
                GDBusMethodInvocation  *invocation,
                gboolean                no_interaction,
                GError                **error)
{
  FlatpakDir *system = NULL;
  const char *sender;
  g_autoptr(AutoPolkitSubject) subject = NULL;

  if (installation != NULL && *installation != '\0')
    system = flatpak_dir_get_system_by_id (installation, NULL, error);
  else
    system = flatpak_dir_get_system_default ();

  /* This can happen in case of error with flatpak_dir_get_system_by_id(). */
  if (system == NULL)
    return NULL;

  sender = g_dbus_method_invocation_get_sender (invocation);
  subject = polkit_system_bus_name_new (sender);

  flatpak_dir_set_subject (system, subject);
  flatpak_dir_set_no_system_helper (system, TRUE);
  flatpak_dir_set_no_interaction (system, no_interaction);

  return system;
}

static void
flatpak_invocation_return_error (GDBusMethodInvocation *invocation,
                                 GError                *error,
                                 const char            *fmt,
                                 ...) G_GNUC_PRINTF (3, 4);

static void
flatpak_invocation_return_error (GDBusMethodInvocation *invocation,
                                 GError                *error,
                                 const char            *fmt,
                                 ...)
{
  if (error->domain == FLATPAK_ERROR)
    g_dbus_method_invocation_return_gerror (invocation, error);
  else
    {
      va_list args;
      g_autofree char *prefix = NULL;
      va_start (args, fmt);
      g_vasprintf (&prefix, fmt, args);
      va_end (args);
      g_dbus_method_invocation_return_error (invocation,
                                             G_DBUS_ERROR, G_DBUS_ERROR_FAILED,
                                             "%s: %s", prefix, error->message);
    }
}

static gboolean
get_connection_uid (GDBusMethodInvocation *invocation, uid_t *out_uid, GError **error)
{
  GDBusConnection *connection = g_dbus_method_invocation_get_connection (invocation);
  const gchar *sender = g_dbus_method_invocation_get_sender (invocation);
  g_autoptr(GVariant) dict = NULL;
  g_autoptr(GVariant) credentials = NULL;

  credentials = g_dbus_connection_call_sync (connection,
                                             "org.freedesktop.DBus",
                                             "/org/freedesktop/DBus",
                                             "org.freedesktop.DBus",
                                             "GetConnectionCredentials",
                                             g_variant_new ("(s)", sender),
                                             G_VARIANT_TYPE ("(a{sv})"), G_DBUS_CALL_FLAGS_NONE,
                                             G_MAXINT, NULL, error);
  if (credentials == NULL)
    return FALSE;

  dict = g_variant_get_child_value (credentials, 0);

   if (!g_variant_lookup (dict, "UnixUserID", "u", out_uid))
    {
      g_set_error (error, G_DBUS_ERROR, G_DBUS_ERROR_FAILED,
                   "Failed to query UnixUserID for the bus name: %s", sender);
      return FALSE;
    }

  return TRUE;
}

static OngoingPull *
take_ongoing_pull_by_dir (const gchar *src_dir)
{
  OngoingPull *pull = NULL;
  gpointer key, value;

  G_LOCK (cache_dirs_in_use);
  /* Keep src_dir key inside hashtable but remove its OngoingPull
   * value and set it to NULL. This way src_dir is still marked
   * as in-use (as Deploy or CancelPull might be executing on it,
   * whereas OngoingPull ownership is transferred to respective
   * callers. */
  if (g_hash_table_steal_extended (cache_dirs_in_use, src_dir, &key, &value))
    {
      if (value)
        {
          g_hash_table_insert (cache_dirs_in_use, key, NULL);
          pull = value;
        }
    }
  G_UNLOCK (cache_dirs_in_use);

  return pull;
}

static gboolean
handle_deploy (FlatpakSystemHelper   *object,
               GDBusMethodInvocation *invocation,
               const gchar           *arg_repo_path,
               guint32                arg_flags,
               const gchar           *arg_ref,
               const gchar           *arg_origin,
               const gchar *const    *arg_subpaths,
               const gchar *const    *arg_previous_ids,
               const gchar           *arg_installation)
{
  g_autoptr(FlatpakDir) system = NULL;
  g_autoptr(GFile) repo_file = g_file_new_for_path (arg_repo_path);
  g_autoptr(GError) error = NULL;
  g_autoptr(GFile) deploy_dir = NULL;
  gboolean is_oci;
  gboolean is_update;
  gboolean no_deploy;
  gboolean local_pull;
  gboolean reinstall;
  gboolean update_pinned;
  gboolean update_preinstalled;
  g_autofree char *url = NULL;
  g_autoptr(OngoingPull) ongoing_pull = NULL;
  g_autofree gchar *src_dir = NULL;
  g_autoptr(FlatpakDecomposed) ref = NULL;

  g_info ("Deploy %s %u %s %s %s", arg_repo_path, arg_flags, arg_ref, arg_origin, arg_installation);

  system = dir_get_system (arg_installation, invocation, (arg_flags & FLATPAK_HELPER_DEPLOY_FLAGS_NO_INTERACTION) != 0, &error);
  if (system == NULL)
    {
      g_dbus_method_invocation_return_gerror (invocation, error);
      return G_DBUS_METHOD_INVOCATION_HANDLED;
    }

  if ((arg_flags & ~FLATPAK_HELPER_DEPLOY_FLAGS_ALL) != 0)
    {
      g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS,
                                             "Unsupported flags enabled: 0x%x", (arg_flags & ~FLATPAK_HELPER_DEPLOY_FLAGS_ALL));
      return G_DBUS_METHOD_INVOCATION_HANDLED;
    }

  if (strlen (arg_repo_path) > 0)
    {
      if (!g_file_query_exists (repo_file, NULL))
        {
          g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS,
                                                 "Path does not exist");
          return G_DBUS_METHOD_INVOCATION_HANDLED;
        }

      src_dir = g_path_get_dirname (arg_repo_path);
      ongoing_pull = take_ongoing_pull_by_dir (src_dir);
      if (ongoing_pull != NULL)
        {
          g_autoptr(GError) local_error = NULL;
          uid_t uid;

          /* Ensure that pull's uid is same as the caller's uid */
          if (!get_connection_uid (invocation, &uid, &local_error))
            {
              g_dbus_method_invocation_return_gerror (invocation, local_error);
              return G_DBUS_METHOD_INVOCATION_HANDLED;
            }
          else
            {
              if (ongoing_pull->uid != uid)
                {
                  g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, G_DBUS_ERROR_FAILED,
                                                         "Ongoing pull's uid(%d) does not match with peer uid(%d)",
                                                         ongoing_pull->uid, uid);
                  return G_DBUS_METHOD_INVOCATION_HANDLED;
                }
            }

          terminate_revokefs_backend (ongoing_pull);

          if (!flatpak_canonicalize_permissions (AT_FDCWD,
                                                 arg_repo_path,
                                                 getuid() == 0 ? 0 : -1,
                                                 getuid() == 0 ? 0 : -1,
                                                 &local_error))
            {
              g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, G_DBUS_ERROR_FAILED,
                                                     "Failed to canonicalize permissions of repo %s: %s",
                                                     arg_repo_path, local_error->message);
              return G_DBUS_METHOD_INVOCATION_HANDLED;
            }

          /* At this point, the cache-dir's repo is owned by root. Hence, any failure
           * from here on, should always cleanup the cache-dir and not preserve it to be re-used. */
          ongoing_pull->preserve_pull = FALSE;
        }
    }

  ref = flatpak_decomposed_new_from_ref (arg_ref, &error);
  if (ref == NULL)
    {
      g_dbus_method_invocation_return_gerror (invocation, error);
      return G_DBUS_METHOD_INVOCATION_HANDLED;
    }

  no_deploy = (arg_flags & FLATPAK_HELPER_DEPLOY_FLAGS_NO_DEPLOY) != 0;
  local_pull = (arg_flags & FLATPAK_HELPER_DEPLOY_FLAGS_LOCAL_PULL) != 0;
  reinstall = (arg_flags & FLATPAK_HELPER_DEPLOY_FLAGS_REINSTALL) != 0;
  update_pinned = (arg_flags & FLATPAK_HELPER_DEPLOY_FLAGS_UPDATE_PINNED) != 0;
  update_preinstalled = (arg_flags & FLATPAK_HELPER_DEPLOY_FLAGS_UPDATE_PREINSTALLED) != 0;

  deploy_dir = flatpak_dir_get_if_deployed (system, ref, NULL, NULL);

  is_update = (deploy_dir && !reinstall);
  if (is_update)
    {
      g_autofree char *real_origin = NULL;
      real_origin = flatpak_dir_get_origin (system, ref, NULL, NULL);
      if (g_strcmp0 (real_origin, arg_origin) != 0)
        {
          g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS,
                                                 "Wrong origin %s for update", arg_origin);
          return G_DBUS_METHOD_INVOCATION_HANDLED;
        }
    }

  if (!flatpak_dir_ensure_repo (system, NULL, &error))
    {
      flatpak_invocation_return_error (invocation, error, "Can't open system repo %s", arg_installation);
      return G_DBUS_METHOD_INVOCATION_HANDLED;
    }

  is_oci = flatpak_dir_get_remote_oci (system, arg_origin);

  if (is_update && !is_oci)
    {
      /* Take this opportunity to clean up refs/mirrors/ since a prune will happen
       * after this update operation. See
       * https://github.com/flatpak/flatpak/issues/3222
       */
      if (!flatpak_dir_delete_mirror_refs (system, FALSE, NULL, &error))
        {
          flatpak_invocation_return_error (invocation, error, "Can't delete mirror refs");
          return G_DBUS_METHOD_INVOCATION_HANDLED;
        }
    }

  if (strlen (arg_repo_path) > 0 && is_oci)
    {
      g_autoptr(GFile) registry_file = g_file_new_for_path (arg_repo_path);
      g_autoptr(FlatpakImageSource) image_source = NULL;
      g_autoptr(FlatpakRemoteState) state = NULL;
      g_autoptr(GHashTable) remote_refs = NULL;
      g_autofree char *checksum = NULL;
      const char *image_source_digest;
      const char *verified_digest;
      g_autofree char *upstream_url = NULL;
      g_autoptr(FlatpakImageSource) system_image_source = NULL;

      ostree_repo_remote_get_url (flatpak_dir_get_repo (system),
                                  arg_origin,
                                  &upstream_url,
                                  NULL);

      if (upstream_url == NULL)
        {
          g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, G_DBUS_ERROR_FAILED,
                                                 "Remote %s is disabled", arg_origin);
          return G_DBUS_METHOD_INVOCATION_HANDLED;
        }

      image_source = flatpak_image_source_new_local (registry_file, arg_ref, NULL, &error);
      if (image_source == NULL)
        {
          g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, G_DBUS_ERROR_FAILED,
                                                 "Can't open child OCI registry: %s", error->message);
          return G_DBUS_METHOD_INVOCATION_HANDLED;
        }

      state = flatpak_dir_get_remote_state (system, arg_origin, FALSE, NULL, &error);
      if (state == NULL)
        {
          g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, G_DBUS_ERROR_FAILED,
                                                 "%s: Can't get remote state: %s", arg_origin, error->message);
          return G_DBUS_METHOD_INVOCATION_HANDLED;
        }

      /* We need to use list_all_remote_refs because we don't care about
       * enumerate vs. noenumerate.
       */
      if (!flatpak_dir_list_all_remote_refs (system, state, &remote_refs, NULL, &error))
        {
          g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, G_DBUS_ERROR_FAILED,
                                                 "%s: Can't list refs: %s", arg_origin, error->message);
          return G_DBUS_METHOD_INVOCATION_HANDLED;
        }

      verified_digest = g_hash_table_lookup (remote_refs, ref);
      if (!verified_digest)
        {
          g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, G_DBUS_ERROR_FAILED,
                                                 "%s: ref %s not found", arg_origin, arg_ref);
          return G_DBUS_METHOD_INVOCATION_HANDLED;
        }

      image_source_digest = flatpak_image_source_get_digest (image_source);

      if (!g_str_has_prefix (image_source_digest, "sha256:") ||
          strcmp (image_source_digest + strlen ("sha256:"), verified_digest) != 0)
        {
          g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, G_DBUS_ERROR_FAILED,
                                                 "%s: manifest hash in downloaded content does not match ref %s", arg_origin, arg_ref);
          return G_DBUS_METHOD_INVOCATION_HANDLED;
        }

      system_image_source =
        flatpak_remote_state_fetch_image_source (state,
                                                 system,
                                                 arg_ref,
                                                 verified_digest,
                                                 NULL,
                                                 NULL, &error);
      if (!system_image_source)
        {
          g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, G_DBUS_ERROR_FAILED,
                                                 "Can't fetch image source: %s", error->message);
          return G_DBUS_METHOD_INVOCATION_HANDLED;
        }

      checksum = flatpak_pull_from_oci (flatpak_dir_get_repo (system), image_source, system_image_source,
                                        arg_origin, arg_ref, FLATPAK_PULL_FLAGS_NONE, NULL, NULL, NULL, &error);
      if (checksum == NULL)
        {
          g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, G_DBUS_ERROR_FAILED,
                                                 "Can't pull ref %s from child OCI registry index: %s", arg_ref, error->message);
          return G_DBUS_METHOD_INVOCATION_HANDLED;
        }

      if (!flatpak_dir_pull_oci_extra_data (flatpak_dir_get_repo (system), image_source,
                                            checksum, NULL, &error))
        {
          g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, G_DBUS_ERROR_FAILED,
                                                 "Can't pull extra data for ref %s from child OCI registry index: %s",
                                                 arg_ref, error->message);
          return G_DBUS_METHOD_INVOCATION_HANDLED;
        }
    }
  else if (strlen (arg_repo_path) > 0)
    {
      if (!flatpak_dir_pull_untrusted_local (system, arg_repo_path,
                                             arg_origin,
                                             arg_ref,
                                             (const char **) arg_subpaths,
                                             NULL, NULL, &error))
        {
          flatpak_invocation_return_error (invocation, error, "Error pulling from repo");
          return G_DBUS_METHOD_INVOCATION_HANDLED;
        }
    }
  else if (local_pull)
    {
      g_autoptr(FlatpakRemoteState) state = NULL;
      if (!ostree_repo_remote_get_url (flatpak_dir_get_repo (system),
                                       arg_origin,
                                       &url,
                                       &error))
        {
          flatpak_invocation_return_error (invocation, error, "Error getting remote url");
          return G_DBUS_METHOD_INVOCATION_HANDLED;
        }

      if (!g_str_has_prefix (url, "file:"))
        {
          g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, G_DBUS_ERROR_FAILED,
                                                 "Local pull url doesn't start with file://");
          return G_DBUS_METHOD_INVOCATION_HANDLED;
        }

      state = flatpak_dir_get_remote_state_optional (system, arg_origin, FALSE, NULL, &error);
      if (state == NULL)
        {
          flatpak_invocation_return_error (invocation, error, "Error getting remote state");
          return G_DBUS_METHOD_INVOCATION_HANDLED;
        }

      if (!flatpak_dir_pull (system, state, arg_ref, NULL, (const char **) arg_subpaths, NULL, NULL, NULL, NULL, NULL,
                             FLATPAK_PULL_FLAGS_NONE, OSTREE_REPO_PULL_FLAGS_UNTRUSTED, NULL,
                             NULL, &error))
        {
          flatpak_invocation_return_error (invocation, error, "Error pulling from repo");
          return G_DBUS_METHOD_INVOCATION_HANDLED;
        }
    }

  if (!no_deploy)
    {
      if (deploy_dir && !reinstall)
        {
          if (!flatpak_dir_deploy_update (system, ref, NULL,
                                          (const char **) arg_subpaths,
                                          (const char **) arg_previous_ids,
                                          NULL, &error))
            {
              flatpak_invocation_return_error (invocation, error, "Error deploying");
              return G_DBUS_METHOD_INVOCATION_HANDLED;
            }
        }
      else
        {
          if (!flatpak_dir_deploy_install (system, ref, arg_origin,
                                           (const char **) arg_subpaths,
                                           (const char **) arg_previous_ids,
                                           reinstall, update_pinned, update_preinstalled,
                                           NULL, &error))
            {
              flatpak_invocation_return_error (invocation, error, "Error deploying");
              return G_DBUS_METHOD_INVOCATION_HANDLED;
            }
        }
    }

  flatpak_system_helper_complete_deploy (object, invocation);

  return G_DBUS_METHOD_INVOCATION_HANDLED;
}

static gboolean
handle_cancel_pull (FlatpakSystemHelper   *object,
                    GDBusMethodInvocation *invocation,
                    guint                  arg_flags,
                    const gchar           *arg_installation,
                    const gchar           *arg_src_dir)
{
  OngoingPull *ongoing_pull;
  g_autoptr(FlatpakDir) system = NULL;
  g_autoptr(GError) error = NULL;
  uid_t uid;

  g_info ("CancelPull %s %u %s", arg_installation, arg_flags, arg_src_dir);

  system = dir_get_system (arg_installation, invocation, (arg_flags & FLATPAK_HELPER_CANCEL_PULL_FLAGS_NO_INTERACTION) != 0, &error);
  if (system == NULL)
    {
      g_dbus_method_invocation_return_gerror (invocation, error);
      return G_DBUS_METHOD_INVOCATION_HANDLED;
    }

  ongoing_pull = take_ongoing_pull_by_dir (arg_src_dir);
  if (ongoing_pull == NULL)
    {
      g_set_error (&error, G_DBUS_ERROR, G_DBUS_ERROR_FAILED,
                   "Cannot find ongoing pull to cancel at %s", arg_src_dir);
      g_dbus_method_invocation_return_gerror (invocation, error);
      return G_DBUS_METHOD_INVOCATION_HANDLED;
    }

  /* Ensure that pull's uid is same as the caller's uid */
  if (!get_connection_uid (invocation, &uid, &error))
    {
      g_dbus_method_invocation_return_gerror (invocation, error);
      return G_DBUS_METHOD_INVOCATION_HANDLED;
    }
  else
    {
      if (ongoing_pull->uid != uid)
        {
          g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, G_DBUS_ERROR_FAILED,
                                                 "Ongoing pull's uid(%d) does not match with peer uid(%d)",
                                                 ongoing_pull->uid, uid);
          return G_DBUS_METHOD_INVOCATION_HANDLED;
        }
    }

  ongoing_pull->preserve_pull = (arg_flags & FLATPAK_HELPER_CANCEL_PULL_FLAGS_PRESERVE_PULL) != 0;
  ongoing_pull_free (ongoing_pull);

  flatpak_system_helper_complete_cancel_pull (object, invocation);
  return G_DBUS_METHOD_INVOCATION_HANDLED;
}

static gboolean
handle_deploy_appstream (FlatpakSystemHelper   *object,
                         GDBusMethodInvocation *invocation,
                         const gchar           *arg_repo_path,
                         guint                  arg_flags,
                         const gchar           *arg_origin,
                         const gchar           *arg_arch,
                         const gchar           *arg_installation)
{
  g_autoptr(FlatpakDir) system = NULL;
  g_autoptr(GError) error = NULL;
  g_autofree char *new_branch = NULL;
  g_autofree char *old_branch = NULL;
  g_autofree char *subset = NULL;
  gboolean is_oci;

  g_info ("DeployAppstream %s %u %s %s %s", arg_repo_path, arg_flags, arg_origin, arg_arch, arg_installation);

  system = dir_get_system (arg_installation, invocation, (arg_flags & FLATPAK_HELPER_DEPLOY_APPSTREAM_FLAGS_NO_INTERACTION) != 0, &error);
  if (system == NULL)
    {
      g_dbus_method_invocation_return_gerror (invocation, error);
      return G_DBUS_METHOD_INVOCATION_HANDLED;
    }

  if (strlen (arg_repo_path) > 0)
    {
      g_autoptr(GFile) repo_file = g_file_new_for_path (arg_repo_path);
      if (!g_file_query_exists (repo_file, NULL))
        {
          g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS,
                                                 "Path does not exist");
          return G_DBUS_METHOD_INVOCATION_HANDLED;
        }
    }

  if (!flatpak_dir_ensure_repo (system, NULL, &error))
    {
      g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, G_DBUS_ERROR_FAILED,
                                             "Can't open system repo %s", error->message);
      return G_DBUS_METHOD_INVOCATION_HANDLED;
    }

  is_oci = flatpak_dir_get_remote_oci (system, arg_origin);

  subset = flatpak_dir_get_remote_subset (system, arg_origin);

  if (subset)
    {
      new_branch = g_strdup_printf ("appstream2/%s-%s", subset, arg_arch);
      old_branch = g_strdup_printf ("appstream/%s-%s", subset, arg_arch);
    }
  else
    {
      new_branch = g_strdup_printf ("appstream2/%s", arg_arch);
      old_branch = g_strdup_printf ("appstream/%s", arg_arch);
    }

  if (is_oci)
    {
      g_auto(FlatpakMainContext) context = FLATKPAK_MAIN_CONTEXT_INIT;

      /* This does http requests spinning the current mainloop, so we need one
         for this thread. */
      flatpak_progress_init_main_context (NULL, &context);
      /* In the OCI case, we just do the full update, including network i/o, in the
       * system helper, see comment in flatpak_dir_update_appstream()
       */
      if (!flatpak_dir_update_appstream (system,
                                         arg_origin,
                                         arg_arch,
                                         NULL,
                                         NULL,
                                         NULL,
                                         &error))
        {
          flatpak_invocation_return_error (invocation, error, "Error updating appstream");
          return G_DBUS_METHOD_INVOCATION_HANDLED;
        }

      flatpak_system_helper_complete_deploy_appstream (object, invocation);
      return G_DBUS_METHOD_INVOCATION_HANDLED;
    }
  else if (strlen (arg_repo_path) > 0)
    {
      g_autoptr(GError) first_error = NULL;
      g_autoptr(GError) second_error = NULL;

      if (!flatpak_dir_pull_untrusted_local (system, arg_repo_path,
                                             arg_origin,
                                             new_branch,
                                             NULL,
                                             NULL,
                                             NULL, &first_error))
        {
          if (!flatpak_dir_pull_untrusted_local (system, arg_repo_path,
                                                 arg_origin,
                                                 old_branch,
                                                 NULL,
                                                 NULL,
                                                 NULL, &second_error))
            {
              g_prefix_error (&first_error, "Error updating appstream2: ");
              g_prefix_error (&second_error, "%s; Error updating appstream: ", first_error->message);
              g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, G_DBUS_ERROR_FAILED,
                                                     "Error pulling from repo: %s", second_error->message);
              return G_DBUS_METHOD_INVOCATION_HANDLED;
            }
        }
    }
  else /* empty path == local pull */
    {
      g_autoptr(FlatpakRemoteState) state = NULL;
      g_autoptr(GError) first_error = NULL;
      g_autoptr(GError) second_error = NULL;
      g_autofree char *url = NULL;

      if (!ostree_repo_remote_get_url (flatpak_dir_get_repo (system),
                                       arg_origin,
                                       &url,
                                       &error))
        {
          flatpak_invocation_return_error (invocation, error, "Error getting remote url");
          return G_DBUS_METHOD_INVOCATION_HANDLED;
        }

      if (!g_str_has_prefix (url, "file:"))
        {
          g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, G_DBUS_ERROR_FAILED,
                                                 "Local pull url doesn't start with file://");
          return G_DBUS_METHOD_INVOCATION_HANDLED;
        }

      state = flatpak_dir_get_remote_state_optional (system, arg_origin, FALSE, NULL, &error);
      if (state == NULL)
        {
          flatpak_invocation_return_error (invocation, error, "Error getting remote state");
          return G_DBUS_METHOD_INVOCATION_HANDLED;
        }

      if (!flatpak_dir_pull (system, state, new_branch, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
                             FLATPAK_PULL_FLAGS_NONE, OSTREE_REPO_PULL_FLAGS_UNTRUSTED, NULL,
                             NULL, &first_error))
        {
          if (!flatpak_dir_pull (system, state, old_branch, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
                                 FLATPAK_PULL_FLAGS_NONE, OSTREE_REPO_PULL_FLAGS_UNTRUSTED, NULL,
                                 NULL, &second_error))
            {
              g_prefix_error (&first_error, "Error updating appstream2: ");
              g_prefix_error (&second_error, "%s; Error updating appstream: ", first_error->message);
              g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, G_DBUS_ERROR_FAILED,
                                                     "Error pulling from repo: %s", second_error->message);
              return G_DBUS_METHOD_INVOCATION_HANDLED;
            }
        }
    }

  if (!flatpak_dir_deploy_appstream (system,
                                     arg_origin,
                                     arg_arch,
                                     NULL,
                                     NULL,
                                     &error))
    {
      flatpak_invocation_return_error (invocation, error, "Error deploying appstream");
      return G_DBUS_METHOD_INVOCATION_HANDLED;
    }

  flatpak_system_helper_complete_deploy_appstream (object, invocation);

  return G_DBUS_METHOD_INVOCATION_HANDLED;
}

static gboolean
handle_uninstall (FlatpakSystemHelper   *object,
                  GDBusMethodInvocation *invocation,
                  guint                  arg_flags,
                  const gchar           *arg_ref,
                  const gchar           *arg_installation)
{
  g_autoptr(FlatpakDir) system = NULL;
  g_autoptr(GError) error = NULL;
  g_autoptr(FlatpakDecomposed) ref = NULL;

  g_info ("Uninstall %u %s %s", arg_flags, arg_ref, arg_installation);

  system = dir_get_system (arg_installation, invocation, (arg_flags & FLATPAK_HELPER_UNINSTALL_FLAGS_NO_INTERACTION) != 0, &error);
  if (system == NULL)
    {
      g_dbus_method_invocation_return_gerror (invocation, error);
      return G_DBUS_METHOD_INVOCATION_HANDLED;
    }

  ref = flatpak_decomposed_new_from_ref (arg_ref, &error);
  if (ref == NULL)
    {
      g_dbus_method_invocation_return_gerror (invocation, error);
      return G_DBUS_METHOD_INVOCATION_HANDLED;
    }

  if ((arg_flags & ~FLATPAK_HELPER_UNINSTALL_FLAGS_ALL) != 0)
    {
      g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS,
                                             "Unsupported flags enabled: 0x%x", (arg_flags & ~FLATPAK_HELPER_UNINSTALL_FLAGS_ALL));
      return G_DBUS_METHOD_INVOCATION_HANDLED;
    }

  if (!flatpak_dir_ensure_repo (system, NULL, &error))
    {
      g_dbus_method_invocation_return_gerror (invocation, error);
      return G_DBUS_METHOD_INVOCATION_HANDLED;
    }

  if (!flatpak_dir_uninstall (system, ref, arg_flags, NULL, &error))
    {
      flatpak_invocation_return_error (invocation, error, "Error uninstalling");
      return G_DBUS_METHOD_INVOCATION_HANDLED;
    }

  flatpak_system_helper_complete_uninstall (object, invocation);

  return G_DBUS_METHOD_INVOCATION_HANDLED;
}

static gboolean
handle_install_bundle (FlatpakSystemHelper   *object,
                       GDBusMethodInvocation *invocation,
                       const gchar           *arg_bundle_path,
                       guint32                arg_flags,
                       const gchar           *arg_remote,
                       const gchar           *arg_installation)
{
  g_autoptr(FlatpakDir) system = NULL;
  g_autoptr(GFile) bundle_file = g_file_new_for_path (arg_bundle_path);
  g_autoptr(GError) error = NULL;
  g_autoptr(FlatpakDecomposed) ref = NULL;
  gboolean reinstall;

  g_info ("InstallBundle %s %u %s %s", arg_bundle_path, arg_flags, arg_remote, arg_installation);

  system = dir_get_system (arg_installation, invocation, (arg_flags & FLATPAK_HELPER_INSTALL_BUNDLE_FLAGS_NO_INTERACTION) != 0, &error);
  if (system == NULL)
    {
      g_dbus_method_invocation_return_gerror (invocation, error);
      return G_DBUS_METHOD_INVOCATION_HANDLED;
    }

  if ((arg_flags & ~FLATPAK_HELPER_INSTALL_BUNDLE_FLAGS_ALL) != 0)
    {
      g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS,
                                             "Unsupported flags enabled: 0x%x", (arg_flags & ~FLATPAK_HELPER_INSTALL_BUNDLE_FLAGS_ALL));
      return G_DBUS_METHOD_INVOCATION_HANDLED;
    }

  if (!g_file_query_exists (bundle_file, NULL))
    {
      g_dbus_method_invocation_return_error (invocation, G_IO_ERROR, G_IO_ERROR_NOT_FOUND,
                                             "Bundle %s does not exist", arg_bundle_path);
      return G_DBUS_METHOD_INVOCATION_HANDLED;
    }

  reinstall = !!(arg_flags & FLATPAK_HELPER_INSTALL_BUNDLE_FLAGS_NO_INTERACTION);
  if (!flatpak_dir_install_bundle (system, reinstall, bundle_file, arg_remote, &ref, NULL, &error))
    {
      flatpak_invocation_return_error (invocation, error, "Error installing bundle");
      return G_DBUS_METHOD_INVOCATION_HANDLED;
    }

  flatpak_system_helper_complete_install_bundle (object, invocation, flatpak_decomposed_get_ref (ref));

  return G_DBUS_METHOD_INVOCATION_HANDLED;
}


static gboolean
handle_configure_remote (FlatpakSystemHelper   *object,
                         GDBusMethodInvocation *invocation,
                         guint                  arg_flags,
                         const gchar           *arg_remote,
                         const gchar           *arg_config,
                         GVariant              *arg_gpg_key,
                         const gchar           *arg_installation)
{
  g_autoptr(FlatpakDir) system = NULL;
  g_autoptr(GError) error = NULL;
  g_autoptr(GKeyFile) config = g_key_file_new ();
  g_autofree char *group = g_strdup_printf ("remote \"%s\"", arg_remote);
  g_autoptr(GBytes) gpg_data = NULL;
  gboolean force_remove;

  g_info ("ConfigureRemote %u %s %s", arg_flags, arg_remote, arg_installation);

  system = dir_get_system (arg_installation, invocation, (arg_flags & FLATPAK_HELPER_CONFIGURE_REMOTE_FLAGS_NO_INTERACTION) != 0, &error);
  if (system == NULL)
    {
      g_dbus_method_invocation_return_gerror (invocation, error);
      return G_DBUS_METHOD_INVOCATION_HANDLED;
    }

  if (*arg_remote == 0 || strchr (arg_remote, '/') != NULL)
    {
      g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS,
                                             "Invalid remote name: %s", arg_remote);
      return G_DBUS_METHOD_INVOCATION_HANDLED;
    }

  if ((arg_flags & ~FLATPAK_HELPER_CONFIGURE_REMOTE_FLAGS_ALL) != 0)
    {
      g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS,
                                             "Unsupported flags enabled: 0x%x", (arg_flags & ~FLATPAK_HELPER_CONFIGURE_REMOTE_FLAGS_ALL));
      return G_DBUS_METHOD_INVOCATION_HANDLED;
    }

  if (!g_key_file_load_from_data (config, arg_config, strlen (arg_config),
                                  G_KEY_FILE_NONE, &error))
    {
      g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS,
                                             "Invalid config: %s\n", error->message);
      return G_DBUS_METHOD_INVOCATION_HANDLED;
    }

  if (!flatpak_dir_ensure_repo (system, NULL, &error))
    {
      g_dbus_method_invocation_return_gerror (invocation, error);
      return G_DBUS_METHOD_INVOCATION_HANDLED;
    }

  if (g_variant_get_size (arg_gpg_key) > 0)
    gpg_data = g_variant_get_data_as_bytes (arg_gpg_key);

  force_remove = (arg_flags & FLATPAK_HELPER_CONFIGURE_REMOTE_FLAGS_FORCE_REMOVE) != 0;

  if (g_key_file_has_group (config, group))
    {
      if (!flatpak_dir_modify_remote (system, arg_remote, config, gpg_data, NULL, &error))
        {
          flatpak_invocation_return_error (invocation, error, "Error modifying remote");
          return G_DBUS_METHOD_INVOCATION_HANDLED;
        }
    }
  else
    {
      if (!flatpak_dir_remove_remote (system, force_remove, arg_remote, NULL, &error))
        {
          flatpak_invocation_return_error (invocation, error, "Error removing remote");
          return G_DBUS_METHOD_INVOCATION_HANDLED;
        }
    }

  flatpak_system_helper_complete_configure_remote (object, invocation);

  return G_DBUS_METHOD_INVOCATION_HANDLED;
}

static gboolean
handle_configure (FlatpakSystemHelper   *object,
                  GDBusMethodInvocation *invocation,
                  guint                  arg_flags,
                  const gchar           *arg_key,
                  const gchar           *arg_value,
                  const gchar           *arg_installation)
{
  g_autoptr(FlatpakDir) system = NULL;
  g_autoptr(GError) error = NULL;

  g_info ("Configure %u %s=%s %s", arg_flags, arg_key, arg_value, arg_installation);

  system = dir_get_system (arg_installation, invocation, (arg_flags & FLATPAK_HELPER_CONFIGURE_FLAGS_NO_INTERACTION) != 0, &error);
  if (system == NULL)
    {
      g_dbus_method_invocation_return_gerror (invocation, error);
      return G_DBUS_METHOD_INVOCATION_HANDLED;
    }

  if ((arg_flags & ~FLATPAK_HELPER_CONFIGURE_FLAGS_ALL) != 0)
    {
      g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS,
                                             "Unsupported flags enabled: 0x%x", (arg_flags & ~FLATPAK_HELPER_CONFIGURE_FLAGS_ALL));
      return G_DBUS_METHOD_INVOCATION_HANDLED;
    }

  if ((strcmp (arg_key, "languages") != 0) &&
      (strcmp (arg_key, "extra-languages") != 0) &&
      (strcmp (arg_key, "masked") != 0) &&
      (strcmp (arg_key, "pinned") != 0))
    {
      g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS,
                                             "Unsupported key: %s", arg_key);
      return G_DBUS_METHOD_INVOCATION_HANDLED;
    }

  if ((arg_flags & FLATPAK_HELPER_CONFIGURE_FLAGS_UNSET) != 0)
    arg_value = NULL;

  if (!flatpak_dir_ensure_repo (system, NULL, &error))
    {
      g_dbus_method_invocation_return_gerror (invocation, error);
      return G_DBUS_METHOD_INVOCATION_HANDLED;
    }

  if (!flatpak_dir_set_config (system, arg_key, arg_value, &error))
    {
      flatpak_invocation_return_error (invocation, error, "Error setting config");
      return G_DBUS_METHOD_INVOCATION_HANDLED;
    }

  flatpak_system_helper_complete_configure (object, invocation);

  return G_DBUS_METHOD_INVOCATION_HANDLED;
}

static gboolean
handle_update_remote (FlatpakSystemHelper   *object,
                      GDBusMethodInvocation *invocation,
                      guint                  arg_flags,
                      const gchar           *arg_remote,
                      const gchar           *arg_installation,
                      const gchar           *arg_summary_path,
                      const gchar           *arg_summary_sig_path)
{
  g_autoptr(FlatpakDir) system = NULL;
  g_autoptr(GError) error = NULL;
  char *summary_data = NULL;
  gsize summary_size;
  g_autoptr(GBytes) summary_bytes = NULL;
  char *summary_sig_data = NULL;
  gsize summary_sig_size;
  g_autoptr(GBytes) summary_sig_bytes = NULL;
  g_autoptr(FlatpakRemoteState) state = NULL;
  gboolean summary_is_index = (arg_flags & FLATPAK_HELPER_UPDATE_REMOTE_FLAGS_SUMMARY_IS_INDEX) != 0;

  g_info ("UpdateRemote %u %s %s %s %s", arg_flags, arg_remote, arg_installation, arg_summary_path, arg_summary_sig_path);

  system = dir_get_system (arg_installation, invocation, (arg_flags & FLATPAK_HELPER_UPDATE_REMOTE_FLAGS_NO_INTERACTION) != 0, &error);
  if (system == NULL)
    {
      g_dbus_method_invocation_return_gerror (invocation, error);
      return G_DBUS_METHOD_INVOCATION_HANDLED;
    }

  if (*arg_remote == 0 || strchr (arg_remote, '/') != NULL)
    {
      g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS,
                                             "Invalid remote name: %s", arg_remote);
      return G_DBUS_METHOD_INVOCATION_HANDLED;
    }

  if ((arg_flags & ~FLATPAK_HELPER_UPDATE_REMOTE_FLAGS_ALL) != 0)
    {
      g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS,
                                             "Unsupported flags enabled: 0x%x", (arg_flags & ~FLATPAK_HELPER_UPDATE_REMOTE_FLAGS_ALL));
      return G_DBUS_METHOD_INVOCATION_HANDLED;
    }

  if (!g_file_get_contents (arg_summary_path, &summary_data, &summary_size, &error))
    {
      g_dbus_method_invocation_return_gerror (invocation, error);
      return G_DBUS_METHOD_INVOCATION_HANDLED;
    }
  summary_bytes = g_bytes_new_take (summary_data, summary_size);

  if (*arg_summary_sig_path != 0)
    {
      if (!g_file_get_contents (arg_summary_sig_path, &summary_sig_data, &summary_sig_size, &error))
        {
          g_dbus_method_invocation_return_gerror (invocation, error);
          return G_DBUS_METHOD_INVOCATION_HANDLED;
        }
      summary_sig_bytes = g_bytes_new_take (summary_sig_data, summary_sig_size);
    }

  if (summary_is_index)
    state = flatpak_dir_get_remote_state_for_index (system, arg_remote, summary_bytes, summary_sig_bytes, NULL, &error);
  else
    state = flatpak_dir_get_remote_state_for_summary (system, arg_remote, summary_bytes, summary_sig_bytes, NULL, &error);
  if (state == NULL)
    {
      flatpak_invocation_return_error (invocation, error, "Error getting remote state");
      return G_DBUS_METHOD_INVOCATION_HANDLED;
    }

  if (summary_sig_bytes == NULL)
    {
      g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS,
                                             "UpdateRemote requires a summary signature");
      return G_DBUS_METHOD_INVOCATION_HANDLED;
    }

  if (!flatpak_dir_update_remote_configuration_for_state (system, state, FALSE, NULL, NULL, &error))
    {
      flatpak_invocation_return_error (invocation, error, "Error updating remote config");
      return G_DBUS_METHOD_INVOCATION_HANDLED;
    }

  flatpak_system_helper_complete_update_remote (object, invocation);

  return G_DBUS_METHOD_INVOCATION_HANDLED;
}

static gboolean
handle_remove_local_ref (FlatpakSystemHelper   *object,
                         GDBusMethodInvocation *invocation,
                         guint                  arg_flags,
                         const gchar           *arg_remote,
                         const gchar           *arg_ref,
                         const gchar           *arg_installation)
{
  g_autoptr(FlatpakDir) system = NULL;
  g_autoptr(GError) error = NULL;

  g_info ("RemoveLocalRef %u %s %s %s", arg_flags, arg_remote, arg_ref, arg_installation);

  system = dir_get_system (arg_installation, invocation, (arg_flags & FLATPAK_HELPER_REMOVE_LOCAL_REF_FLAGS_NO_INTERACTION) != 0, &error);
  if (system == NULL)
    {
      g_dbus_method_invocation_return_gerror (invocation, error);
      return G_DBUS_METHOD_INVOCATION_HANDLED;
    }

  if ((arg_flags & ~FLATPAK_HELPER_REMOVE_LOCAL_REF_FLAGS_ALL) != 0)
    {
      g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS,
                                             "Unsupported flags enabled: 0x%x", (arg_flags & ~FLATPAK_HELPER_REMOVE_LOCAL_REF_FLAGS_ALL));
      return G_DBUS_METHOD_INVOCATION_HANDLED;
    }

  if (*arg_remote == 0 || strchr (arg_remote, '/') != NULL)
    {
      g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS,
                                             "Invalid remote name: %s", arg_remote);
      return G_DBUS_METHOD_INVOCATION_HANDLED;
    }

  if (!flatpak_dir_ensure_repo (system, NULL, &error))
    {
      g_dbus_method_invocation_return_gerror (invocation, error);
      return G_DBUS_METHOD_INVOCATION_HANDLED;
    }

  if (!flatpak_dir_remove_ref (system, arg_remote, arg_ref, NULL, &error))
    {
      flatpak_invocation_return_error (invocation, error, "Error removing ref");
      return G_DBUS_METHOD_INVOCATION_HANDLED;
    }

  flatpak_system_helper_complete_remove_local_ref (object, invocation);

  return G_DBUS_METHOD_INVOCATION_HANDLED;
}

static gboolean
handle_prune_local_repo (FlatpakSystemHelper   *object,
                         GDBusMethodInvocation *invocation,
                         guint                  arg_flags,
                         const gchar           *arg_installation)
{
  g_autoptr(FlatpakDir) system = NULL;
  g_autoptr(GError) error = NULL;

  g_info ("PruneLocalRepo %u %s", arg_flags, arg_installation);

  system = dir_get_system (arg_installation, invocation, (arg_flags & FLATPAK_HELPER_PRUNE_LOCAL_REPO_FLAGS_NO_INTERACTION) != 0, &error);
  if (system == NULL)
    {
      g_dbus_method_invocation_return_gerror (invocation, error);
      return G_DBUS_METHOD_INVOCATION_HANDLED;
    }

  if ((arg_flags & ~FLATPAK_HELPER_PRUNE_LOCAL_REPO_FLAGS_ALL) != 0)
    {
      g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS,
                                             "Unsupported flags enabled: 0x%x", (arg_flags & ~FLATPAK_HELPER_PRUNE_LOCAL_REPO_FLAGS_ALL));
      return G_DBUS_METHOD_INVOCATION_HANDLED;
    }

  if (!flatpak_dir_ensure_repo (system, NULL, &error))
    {
      g_dbus_method_invocation_return_gerror (invocation, error);
      return G_DBUS_METHOD_INVOCATION_HANDLED;
    }

  if (!flatpak_dir_prune (system, NULL, &error))
    {
      flatpak_invocation_return_error (invocation, error, "Error pruning repo");
      return G_DBUS_METHOD_INVOCATION_HANDLED;
    }

  flatpak_system_helper_complete_prune_local_repo (object, invocation);

  return G_DBUS_METHOD_INVOCATION_HANDLED;
}


static gboolean
handle_ensure_repo (FlatpakSystemHelper   *object,
                    GDBusMethodInvocation *invocation,
                    guint                  arg_flags,
                    const gchar           *arg_installation)
{
  g_autoptr(FlatpakDir) system = NULL;
  g_autoptr(GError) error = NULL;
  g_autoptr(GError) local_error = NULL;

  g_info ("EnsureRepo %u %s", arg_flags, arg_installation);

  system = dir_get_system (arg_installation, invocation, (arg_flags & FLATPAK_HELPER_ENSURE_REPO_FLAGS_NO_INTERACTION) != 0, &error);
  if (system == NULL)
    {
      g_dbus_method_invocation_return_gerror (invocation, error);
      return G_DBUS_METHOD_INVOCATION_HANDLED;
    }

  if ((arg_flags & ~FLATPAK_HELPER_ENSURE_REPO_FLAGS_ALL) != 0)
    {
      g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS,
                                             "Unsupported flags enabled: 0x%x", (arg_flags & ~FLATPAK_HELPER_ENSURE_REPO_FLAGS_ALL));
      return G_DBUS_METHOD_INVOCATION_HANDLED;
    }

  if (!flatpak_dir_ensure_repo (system, NULL, &error))
    {
      g_dbus_method_invocation_return_gerror (invocation, error);
      return G_DBUS_METHOD_INVOCATION_HANDLED;
    }

  if (!flatpak_dir_migrate_config (system, NULL, NULL, &local_error))
    g_warning ("Failed to migrate configuration for installation %s: %s", arg_installation, local_error->message);

  flatpak_system_helper_complete_ensure_repo (object, invocation);

  return G_DBUS_METHOD_INVOCATION_HANDLED;
}

static gboolean
handle_run_triggers (FlatpakSystemHelper   *object,
                     GDBusMethodInvocation *invocation,
                     guint                  arg_flags,
                     const gchar           *arg_installation)
{
  g_autoptr(FlatpakDir) system = NULL;
  g_autoptr(GError) error = NULL;

  g_info ("RunTriggers %u %s", arg_flags, arg_installation);

  system = dir_get_system (arg_installation, invocation, (arg_flags & FLATPAK_HELPER_RUN_TRIGGERS_FLAGS_NO_INTERACTION) != 0, &error);
  if (system == NULL)
    {
      g_dbus_method_invocation_return_gerror (invocation, error);
      return G_DBUS_METHOD_INVOCATION_HANDLED;
    }

  if ((arg_flags & ~FLATPAK_HELPER_RUN_TRIGGERS_FLAGS_ALL) != 0)
    {
      g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS,
                                             "Unsupported flags enabled: 0x%x", (arg_flags & ~FLATPAK_HELPER_RUN_TRIGGERS_FLAGS_ALL));
      return G_DBUS_METHOD_INVOCATION_HANDLED;
    }

  if (!flatpak_dir_ensure_repo (system, NULL, &error))
    {
      g_dbus_method_invocation_return_gerror (invocation, error);
      return G_DBUS_METHOD_INVOCATION_HANDLED;
    }

  if (!flatpak_dir_run_triggers (system, NULL, &error))
    {
      flatpak_invocation_return_error (invocation, error, "Error running triggers");
      return G_DBUS_METHOD_INVOCATION_HANDLED;
    }

  flatpak_system_helper_complete_run_triggers (object, invocation);

  return G_DBUS_METHOD_INVOCATION_HANDLED;
}

static gboolean
check_for_system_helper_user (struct passwd  *passwd,
                              gchar         **passwd_buf,
                              GError        **error)
{
  struct passwd *result = NULL;
  g_autofree gchar *buf = NULL;
  size_t bufsize;
  int err;

  bufsize = sysconf (_SC_GETPW_R_SIZE_MAX);
  if (bufsize == -1)          /* Value was indeterminate */
     bufsize = 16384;        /* Should be more than enough */

  while (!result)
    {
      buf = g_malloc0 (bufsize);
      err = getpwnam_r (SYSTEM_HELPER_USER, passwd, buf, bufsize, &result);
      if (result == NULL)
        {
          if (err == ERANGE)     /* Insufficient buffer space */
            {
              g_free (buf);
              bufsize *= 2;
              continue;
            }
          else if (err == 0)     /* User SYSTEM_HELPER_USER 's record was not found*/
            {
              g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND,
                           "User %s does not exist in password file entry", SYSTEM_HELPER_USER);
              return FALSE;
            }
          else
            {
              g_set_error (error, G_IO_ERROR, g_io_error_from_errno (err),
                           "Failed to query user %s from password file entry", SYSTEM_HELPER_USER);
              return FALSE;
            }
        }
    }

  *passwd_buf = g_steal_pointer (&buf);

  return TRUE;
}

static void
revokefs_fuse_backend_child_setup (gpointer user_data)
{
  struct passwd *passwd = user_data;

  /* We use 5 instead of 3 here, because fd 3 is the inherited SOCK_SEQPACKET
   * socket and fd 4 is the --close-with-fd pipe; both were dup2()'d into place
   * before this by GSubprocess */
  g_fdwalk_set_cloexec (5);

  if (setgid (passwd->pw_gid) == -1)
    {
      g_warning ("Failed to setgid(%d) for revokefs backend: %s",
                 passwd->pw_gid, g_strerror (errno));
      exit (1);
    }

  if (setuid (passwd->pw_uid) == -1)
    {
      g_warning ("Failed to setuid(%d) for revokefs backend: %s",
                 passwd->pw_uid, g_strerror (errno));
      exit (1);
    }
}

static void
name_vanished_cb (GDBusConnection *connection, const gchar *name, gpointer user_data)
{
  const gchar *unique_name = (const gchar *) user_data;
  g_autoptr(GPtrArray) cleanup_pulls = NULL;
  GHashTableIter iter;
  gpointer value;

  cleanup_pulls = g_ptr_array_new_with_free_func ((GDestroyNotify) ongoing_pull_free);

  G_LOCK (cache_dirs_in_use);
  g_hash_table_iter_init (&iter, cache_dirs_in_use);
  while (g_hash_table_iter_next (&iter, NULL, &value))
    {
      OngoingPull *pull = (OngoingPull *) value;
      if (g_strcmp0 (pull->unique_name, unique_name) == 0)
        {
          g_ptr_array_add (cleanup_pulls, pull);
          g_hash_table_iter_remove (&iter);
        }
    }
  G_UNLOCK (cache_dirs_in_use);
}

static OngoingPull *
ongoing_pull_new (FlatpakSystemHelper   *object,
                  GDBusMethodInvocation *invocation,
                  struct passwd         *passwd,
                  uid_t                  uid,
                  const gchar           *src,
                  GError               **error)
{
  GDBusConnection *connection = g_dbus_method_invocation_get_connection (invocation);
  g_autoptr(OngoingPull) pull = NULL;
  g_autoptr(GSubprocessLauncher) launcher = NULL;
  int sockets[2], exit_sockets[2];
  const char *revokefs_fuse_bin = LIBEXECDIR "/revokefs-fuse";

  pull = g_slice_new0 (OngoingPull);
  pull->object = object;
  pull->invocation = invocation;
  pull->src_dir = g_strdup (src);
  pull->cancellable = g_cancellable_new ();
  pull->uid = uid;
  pull->preserve_pull = FALSE;
  pull->unique_name = g_strdup (g_dbus_connection_get_unique_name (connection));

  pull->watch_id = g_bus_watch_name_on_connection (connection,
                                                   pull->unique_name,
                                                   G_BUS_NAME_WATCHER_FLAGS_NONE, NULL,
                                                   name_vanished_cb,
                                                   g_strdup (g_dbus_connection_get_unique_name (connection)),
                                                   g_free);

  if (socketpair (AF_UNIX, SOCK_SEQPACKET, 0, sockets) == -1)
    {
      glnx_throw_errno_prefix (error, "Failed to get a socketpair");
      return NULL;
    }

  if (pipe2 (exit_sockets, O_CLOEXEC) == -1)
    {
      glnx_throw_errno_prefix (error, "Failed to create a pipe");
      close (sockets[0]);
      close (sockets[1]);
      return NULL;
    }

  /* We use INHERIT_FDS and close them in the child_setup
   * to work around a deadlock in GLib < 2.60 */
  launcher = g_subprocess_launcher_new (G_SUBPROCESS_FLAGS_INHERIT_FDS);
  g_subprocess_launcher_set_child_setup (launcher, revokefs_fuse_backend_child_setup, passwd, NULL);
  g_subprocess_launcher_take_fd (launcher, sockets[0], 3);
  fcntl (sockets[1], F_SETFD, FD_CLOEXEC);
  pull->client_socket = sockets[1];

  g_subprocess_launcher_take_fd (launcher, exit_sockets[0], 4);
  pull->backend_exit_socket = exit_sockets[1];

  if (g_getenv ("FLATPAK_REVOKEFS_FUSE"))
    revokefs_fuse_bin = g_getenv ("FLATPAK_REVOKEFS_FUSE");

  pull->revokefs_backend = g_subprocess_launcher_spawn (launcher,
                                                        error,
                                                        revokefs_fuse_bin,
                                                        "--backend",
                                                        "--socket=3",
                                                        "--exit-with-fd=4",
                                                        src, NULL);
  if (pull->revokefs_backend == NULL)
    return NULL;

  return g_steal_pointer (&pull);
}

static gboolean
reuse_cache_dir_if_available (const gchar    *repo_tmp,
                              gchar         **out_src_dir,
                              struct passwd  *passwd)
{
  g_autoptr(GFileEnumerator) enumerator = NULL;
  g_autoptr(GFile) repo_tmpfile = NULL;
  GFileInfo *file_info = NULL;
  g_autoptr(GError) error = NULL;
  const gchar *name;
  gboolean res = FALSE;

  g_info ("Checking for any temporary cache directory available to reuse");

  repo_tmpfile = g_file_new_for_path (repo_tmp);
  enumerator = g_file_enumerate_children (repo_tmpfile,
                                          G_FILE_ATTRIBUTE_STANDARD_NAME ","
                                          G_FILE_ATTRIBUTE_STANDARD_TYPE,
                                          G_FILE_QUERY_INFO_NONE, NULL, &error);
  if (enumerator == NULL)
    {
      g_warning ("Failed to enumerate %s: %s", repo_tmp, error->message);
      return FALSE;
    }

  while (TRUE)
    {
      if (!g_file_enumerator_iterate (enumerator, &file_info, NULL, NULL, &error))
        {
          g_warning ("Error while iterating %s: %s", repo_tmp, error->message);
          break;
        }

      if (file_info == NULL || res == TRUE)
        break;

      name = g_file_info_get_name (file_info);
      if (g_file_info_get_file_type (file_info) == G_FILE_TYPE_DIRECTORY &&
          g_str_has_prefix (name, "flatpak-cache-"))
        {
          g_autoptr(GFile) cache_dir_file = g_file_get_child (repo_tmpfile, name);
          g_autofree gchar *cache_dir_name = g_file_get_path (cache_dir_file);

          G_LOCK (cache_dirs_in_use);
          if (!g_hash_table_contains (cache_dirs_in_use, cache_dir_name))
            {
              struct stat st_buf;

              /* We are able to find a cache dir which is not in use. */
              if (stat (cache_dir_name, &st_buf) == 0 &&
                  st_buf.st_uid == passwd->pw_uid &&        /* should be owned by SYSTEM_HELPER_USER */
                  (st_buf.st_mode & 0022) == 0)             /* should not be world-writeable */
                {
                  gboolean did_not_exist = g_hash_table_insert (cache_dirs_in_use,
                                                                g_strdup (cache_dir_name),
                                                                NULL);
                  g_assert (did_not_exist);
                  *out_src_dir = g_steal_pointer (&cache_dir_name);
                  res = TRUE;
                }
            }
          G_UNLOCK (cache_dirs_in_use);
        }
    }

  return res;
}

static gboolean
handle_get_revokefs_fd (FlatpakSystemHelper   *object,
                        GDBusMethodInvocation *invocation,
                        GUnixFDList           *arg_fdlist,
                        guint                  arg_flags,
                        const gchar           *arg_installation)
{
  g_autoptr(FlatpakDir) system = NULL;
  g_autoptr(GUnixFDList) fd_list = NULL;
  g_autoptr(GError) error = NULL;
  g_autofree gchar *src_dir = NULL;
  g_autofree gchar *flatpak_dir = NULL;
  g_autofree gchar *repo_tmp = NULL;
  g_autofree gchar *passwd_buf = NULL;
  struct passwd passwd = { NULL };
  OngoingPull *new_pull;
  uid_t uid;
  int fd_index = -1;

  g_info ("GetRevokefsFd %u %s", arg_flags, arg_installation);

  if (disable_revokefs)
    {
      g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, G_DBUS_ERROR_NOT_SUPPORTED, "RevokeFS disabled");
      return G_DBUS_METHOD_INVOCATION_HANDLED;
    }

  system = dir_get_system (arg_installation, invocation, (arg_flags & FLATPAK_HELPER_GET_REVOKEFS_FD_FLAGS_NO_INTERACTION) != 0, &error);
  if (system == NULL)
    {
      g_dbus_method_invocation_return_gerror (invocation, error);
      return G_DBUS_METHOD_INVOCATION_HANDLED;
    }

  if ((arg_flags & ~FLATPAK_HELPER_GET_REVOKEFS_FD_FLAGS_ALL) != 0)
    {
      g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS,
                                             "Unsupported flags enabled: 0x%x", (arg_flags & ~FLATPAK_HELPER_GET_REVOKEFS_FD_FLAGS_ALL));
      return G_DBUS_METHOD_INVOCATION_HANDLED;
    }

  if (on_session_bus)
    {
      passwd.pw_uid = getuid();
      passwd.pw_gid = getgid();
    }
  else if (!check_for_system_helper_user (&passwd, &passwd_buf, &error))
    {
      g_dbus_method_invocation_return_gerror (invocation, error);
      return G_DBUS_METHOD_INVOCATION_HANDLED;
    }

  if (!get_connection_uid (invocation, &uid, &error))
    {
      g_dbus_method_invocation_return_gerror (invocation, error);
      return G_DBUS_METHOD_INVOCATION_HANDLED;
    }

  flatpak_dir = g_file_get_path (flatpak_dir_get_path (system));
  repo_tmp = g_build_filename (flatpak_dir, "repo", "tmp", NULL);

   if (reuse_cache_dir_if_available (repo_tmp, &src_dir, &passwd))
     g_info ("Cache dir %s can be reused", src_dir);
  else
    {
      /* Create a new cache dir and add it to cache_dirs_in_use. Do all this under
       * a lock, so that a different pull does not snatch this directory up using
       * reuse_cache_dir_if_available. */
      G_LOCK (cache_dirs_in_use);
      src_dir = g_mkdtemp_full (g_build_filename (repo_tmp, "flatpak-cache-XXXXXX", NULL), 0755);
      if (src_dir == NULL)
        {
          G_UNLOCK (cache_dirs_in_use);
          glnx_throw_errno_prefix (&error, "Failed to create new cache-dir at %s", repo_tmp);
          g_dbus_method_invocation_return_gerror (invocation, error);
          return G_DBUS_METHOD_INVOCATION_HANDLED;
        }
      g_hash_table_insert (cache_dirs_in_use, g_strdup (src_dir), NULL);
      G_UNLOCK (cache_dirs_in_use);

      if (chown (src_dir, passwd.pw_uid, passwd.pw_gid) == -1)
        {
          remove_dir_from_cache_dirs_in_use (src_dir);
          glnx_throw_errno_prefix (&error, "Failed to chown %s to user %s",
                                   src_dir, passwd.pw_name);
          g_dbus_method_invocation_return_gerror (invocation, error);
          return G_DBUS_METHOD_INVOCATION_HANDLED;
        }
    }

  new_pull = ongoing_pull_new (object, invocation, &passwd, uid, src_dir, &error);
  if (error != NULL)
    {
      remove_dir_from_cache_dirs_in_use (src_dir);
      g_dbus_method_invocation_return_gerror (invocation, error);
      return G_DBUS_METHOD_INVOCATION_HANDLED;
    }

  G_LOCK (cache_dirs_in_use);
  g_hash_table_insert (cache_dirs_in_use, g_strdup (src_dir), new_pull);
  G_UNLOCK (cache_dirs_in_use);

  fd_list = g_unix_fd_list_new ();
  fd_index = g_unix_fd_list_append (fd_list, new_pull->client_socket, NULL);

  flatpak_system_helper_complete_get_revokefs_fd (object, invocation,
                                                  fd_list, g_variant_new_handle (fd_index),
                                                  new_pull->src_dir);

  return G_DBUS_METHOD_INVOCATION_HANDLED;
}

static gboolean
handle_update_summary (FlatpakSystemHelper   *object,
                       GDBusMethodInvocation *invocation,
                       guint                  arg_flags,
                       const gchar           *arg_installation)
{
  g_autoptr(FlatpakDir) system = NULL;
  g_autoptr(GError) error = NULL;
  gboolean delete_summary;

  g_info ("UpdateSummary %u %s", arg_flags, arg_installation);

  system = dir_get_system (arg_installation, invocation, (arg_flags & FLATPAK_HELPER_UPDATE_SUMMARY_FLAGS_NO_INTERACTION) != 0, &error);
  if (system == NULL)
    {
      g_dbus_method_invocation_return_gerror (invocation, error);
      return G_DBUS_METHOD_INVOCATION_HANDLED;
    }

  if ((arg_flags & ~FLATPAK_HELPER_UPDATE_SUMMARY_FLAGS_ALL) != 0)
    {
      g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS,
                                             "Unsupported flags enabled: 0x%x", (arg_flags & ~FLATPAK_HELPER_UPDATE_SUMMARY_FLAGS_ALL));
      return G_DBUS_METHOD_INVOCATION_HANDLED;
    }

  if (!flatpak_dir_ensure_repo (system, NULL, &error))
    {
      g_dbus_method_invocation_return_gerror (invocation, error);
      return G_DBUS_METHOD_INVOCATION_HANDLED;
    }
  delete_summary = (arg_flags & FLATPAK_HELPER_UPDATE_SUMMARY_FLAGS_DELETE) != 0;
  if (!flatpak_dir_update_summary (system, delete_summary, NULL, &error))
    {
      flatpak_invocation_return_error (invocation, error, "Error %s summary",
                                       delete_summary ? "deleting" : "updating");
      return G_DBUS_METHOD_INVOCATION_HANDLED;
    }

  flatpak_system_helper_complete_update_summary (object, invocation);

  return G_DBUS_METHOD_INVOCATION_HANDLED;
}

static gboolean
handle_generate_oci_summary (FlatpakSystemHelper   *object,
                             GDBusMethodInvocation *invocation,
                             guint                  arg_flags,
                             const gchar           *arg_origin,
                             const gchar           *arg_installation)
{
  g_autoptr(FlatpakDir) system = NULL;
  g_autoptr(GError) error = NULL;
  gboolean only_cached;
  gboolean is_oci;

  g_info ("GenerateOciSummary %u %s %s", arg_flags, arg_origin, arg_installation);

  system = dir_get_system (arg_installation, invocation, (arg_flags & FLATPAK_HELPER_GENERATE_OCI_SUMMARY_FLAGS_NO_INTERACTION) != 0, &error);
  if (system == NULL)
    {
      g_dbus_method_invocation_return_gerror (invocation, error);
      return G_DBUS_METHOD_INVOCATION_HANDLED;
    }

  if ((arg_flags & ~FLATPAK_HELPER_GENERATE_OCI_SUMMARY_FLAGS_ALL) != 0)
    {
      g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS,
                                             "Unsupported flags enabled: 0x%x", (arg_flags & ~FLATPAK_HELPER_GENERATE_OCI_SUMMARY_FLAGS_ALL));
      return G_DBUS_METHOD_INVOCATION_HANDLED;
    }

  only_cached = (arg_flags & FLATPAK_HELPER_GENERATE_OCI_SUMMARY_FLAGS_ONLY_CACHED) != 0;

  if (!flatpak_dir_ensure_repo (system, NULL, &error))
    {
      g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, G_DBUS_ERROR_FAILED,
                                             "Can't open system repo %s", error->message);
      return G_DBUS_METHOD_INVOCATION_HANDLED;
    }

  is_oci = flatpak_dir_get_remote_oci (system, arg_origin);
  if (!is_oci)
    {
      g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS,
                                             "%s is not a OCI remote", arg_origin);
      return G_DBUS_METHOD_INVOCATION_HANDLED;
    }

  if (!flatpak_dir_remote_make_oci_summary (system, arg_origin, only_cached, NULL, NULL, &error))
    {
      flatpak_invocation_return_error (invocation, error, "Failed to update OCI summary");
      return G_DBUS_METHOD_INVOCATION_HANDLED;
    }


  flatpak_system_helper_complete_generate_oci_summary (object, invocation);

  return G_DBUS_METHOD_INVOCATION_HANDLED;
}

static gboolean
dir_ref_is_installed (FlatpakDir *dir,
                      FlatpakDecomposed *ref)
{
  g_autoptr(GBytes) deploy_data = NULL;

  deploy_data = flatpak_dir_get_deploy_data (dir, ref, FLATPAK_DEPLOY_VERSION_ANY, NULL, NULL);

  return deploy_data != NULL;
}

static gboolean
flatpak_authorize_method_handler (GDBusInterfaceSkeleton *interface,
                                  GDBusMethodInvocation  *invocation,
                                  gpointer                user_data)
{
  const gchar *method_name = g_dbus_method_invocation_get_method_name (invocation);
  const gchar *sender = g_dbus_method_invocation_get_sender (invocation);
  GVariant *parameters = g_dbus_method_invocation_get_parameters (invocation);
  g_autoptr(AutoPolkitSubject) subject = polkit_system_bus_name_new (sender);
  g_autoptr(AutoPolkitDetails) details = polkit_details_new ();
  const gchar *action = NULL;
  gboolean authorized = FALSE;
  gboolean no_interaction = FALSE;

  /* Ensure we don't idle exit */
  schedule_idle_callback ();

  if (on_session_bus)
    {
      /* This is test code, make sure it never runs with privileges */
      g_assert (geteuid () != 0);
      g_assert (getuid () != 0);
      g_assert (getegid () != 0);
      g_assert (getgid () != 0);
      authorized = TRUE;
    }
  else if (g_strcmp0 (method_name, "Deploy") == 0)
    {
      const char *installation;
      const char *ref_str, *origin;
      guint32 flags;

      g_variant_get_child (parameters, 1, "u", &flags);
      g_variant_get_child (parameters, 2, "&s", &ref_str);
      g_variant_get_child (parameters, 3, "&s", &origin);
      g_variant_get_child (parameters, 6, "&s", &installation);

      /* For metadata updates, redirect to the metadata-update action which
       * should basically always be allowed */
      if (ref_str != NULL && g_strcmp0 (ref_str, OSTREE_REPO_METADATA_REF) == 0)
        {
          action = "org.freedesktop.Flatpak.metadata-update";
        }
      else
        {
          g_autoptr(GError) error = NULL;
          g_autoptr(FlatpakDecomposed) ref = NULL;
          gboolean is_app, is_install;

          ref = flatpak_decomposed_new_from_ref (ref_str, &error);
          if (ref == NULL)
            {
              g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, G_DBUS_ERROR_FAILED,
                                                     "Error deployings: %s", error->message);
              return FALSE;
            }

          no_interaction = (flags & FLATPAK_HELPER_DEPLOY_FLAGS_NO_INTERACTION) != 0;

          /* These flags allow clients to "upgrade" the permission,
           * avoiding the need for multiple polkit dialogs when we first
           * update a runtime, then install the app that needs it.
           *
           * Note that our policy has implications:
           * app-install > app-update > runtime-install > runtime-update
           * which means that these hints only ever select a stronger
           * permission, and are safe in that sense.
           */

          if ((flags & FLATPAK_HELPER_DEPLOY_FLAGS_APP_HINT) != 0)
            is_app = TRUE;
          else
            is_app = flatpak_decomposed_is_app (ref);

          if ((flags & FLATPAK_HELPER_DEPLOY_FLAGS_INSTALL_HINT) != 0 ||
              (flags & FLATPAK_HELPER_DEPLOY_FLAGS_REINSTALL) != 0)
            is_install = TRUE;
          else
            {
              g_autoptr(FlatpakDir) system = dir_get_system (installation,
                                                             invocation,
                                                             no_interaction,
                                                             &error);

              if (system == NULL)
                {
                  g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, G_DBUS_ERROR_FAILED,
                                                         "Error getting installation %s: %s", installation, error->message);
                  return FALSE;
                }

              is_install = !dir_ref_is_installed (system, ref);
            }

          if (is_install)
            {
              if (is_app)
                action = "org.freedesktop.Flatpak.app-install";
              else
                action = "org.freedesktop.Flatpak.runtime-install";
            }
          else
            {
              if (is_app)
                action = "org.freedesktop.Flatpak.app-update";
              else
                action = "org.freedesktop.Flatpak.runtime-update";
            }
        }

      polkit_details_insert (details, "origin", origin);
      polkit_details_insert (details, "ref", ref_str);
    }
  else if (g_strcmp0 (method_name, "DeployAppstream") == 0)
    {
      guint32 flags;
      const char *arch, *origin;

      g_variant_get_child (parameters, 1, "u", &flags);
      g_variant_get_child (parameters, 2, "&s", &origin);
      g_variant_get_child (parameters, 3, "&s", &arch);

      action = "org.freedesktop.Flatpak.appstream-update";
      no_interaction = (flags & FLATPAK_HELPER_DEPLOY_APPSTREAM_FLAGS_NO_INTERACTION) != 0;

      polkit_details_insert (details, "origin", origin);
      polkit_details_insert (details, "arch", arch);
    }
  else if (g_strcmp0 (method_name, "InstallBundle") == 0)
    {
      const char *path;
      guint32 flags;

      g_variant_get_child (parameters, 0, "^&ay", &path);
      g_variant_get_child (parameters, 1, "u", &flags);

      action = "org.freedesktop.Flatpak.install-bundle";
      no_interaction = (flags & FLATPAK_HELPER_INSTALL_BUNDLE_FLAGS_NO_INTERACTION) != 0;

      polkit_details_insert (details, "path", path);
    }
  else if (g_strcmp0 (method_name, "Uninstall") == 0)
    {
      const char *installation;
      const char *ref_str;
      gboolean is_app;
      guint32 flags;
      g_autoptr(GError) error = NULL;
      g_autoptr(FlatpakDecomposed) ref = NULL;

      g_variant_get_child (parameters, 0, "u", &flags);
      g_variant_get_child (parameters, 1, "&s", &ref_str);
      g_variant_get_child (parameters, 2, "&s", &installation);

      ref = flatpak_decomposed_new_from_ref (ref_str, &error);
      if (ref == NULL)
        {
          g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, G_DBUS_ERROR_FAILED,
                                                 "Error uninstalling: %s", error->message);
          return FALSE;
        }

      is_app = flatpak_decomposed_is_app (ref);
      if (is_app)
        action = "org.freedesktop.Flatpak.app-uninstall";
      else
        action = "org.freedesktop.Flatpak.runtime-uninstall";
      no_interaction = (flags & FLATPAK_HELPER_UNINSTALL_FLAGS_NO_INTERACTION) != 0;

      /* For the app-uninstall action the message shown to the user includes
       * the app, so let's get the human friendly name if possible. Unlike some
       * of the other actions, app-uninstall isn't implied by any other, and
       * only implies runtime-uninstall, so it seems alright to include what
       * app is being uninstalled. */
      if (is_app)
        {
          g_autoptr(FlatpakDir) system = NULL;
          g_autoptr(GBytes) deploy_data = NULL;
          g_autoptr(GError) sys_error = NULL;
          const char *name = NULL;

          system = dir_get_system (installation, invocation, no_interaction, &sys_error);
          if (system == NULL)
            {
              g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, G_DBUS_ERROR_FAILED,
                                                     "Error getting installation %s: %s", installation, sys_error->message);
              return FALSE;
            }

          deploy_data = flatpak_dir_get_deploy_data (system, ref, FLATPAK_DEPLOY_VERSION_CURRENT, NULL, NULL);
          if (deploy_data != NULL)
            name = flatpak_deploy_data_get_appdata_name (deploy_data);
          if (name != NULL && *name != '\0')
            polkit_details_insert (details, "ref", name);
          else
            polkit_details_insert (details, "ref", flatpak_decomposed_get_ref (ref));
        }
      else
        polkit_details_insert (details, "ref", flatpak_decomposed_get_ref (ref));
    }
  else if (g_strcmp0 (method_name, "ConfigureRemote") == 0)
    {
      const char *remote;
      guint32 flags;

      g_variant_get_child (parameters, 0, "u", &flags);
      g_variant_get_child (parameters, 1, "&s", &remote);

      action = "org.freedesktop.Flatpak.configure-remote";
      no_interaction = (flags & FLATPAK_HELPER_CONFIGURE_REMOTE_FLAGS_NO_INTERACTION) != 0;

      polkit_details_insert (details, "remote", remote);
    }
  else if (g_strcmp0 (method_name, "Configure") == 0)
    {
      const char *key;
      guint32 flags;

      g_variant_get_child (parameters, 0, "u", &flags);
      g_variant_get_child (parameters, 1, "&s", &key);

      action = "org.freedesktop.Flatpak.configure";
      no_interaction = (flags & FLATPAK_HELPER_CONFIGURE_FLAGS_NO_INTERACTION) != 0;

      polkit_details_insert (details, "key", key);
    }
  else if (g_strcmp0 (method_name, "UpdateRemote") == 0)
    {
      const char *remote;
      guint32 flags;

      g_variant_get_child (parameters, 0, "u", &flags);
      g_variant_get_child (parameters, 1, "&s", &remote);

      action = "org.freedesktop.Flatpak.update-remote";
      no_interaction = (flags & FLATPAK_HELPER_UPDATE_REMOTE_FLAGS_NO_INTERACTION) != 0;

      polkit_details_insert (details, "remote", remote);
    }
  else if (g_strcmp0 (method_name, "RemoveLocalRef") == 0 ||
           g_strcmp0 (method_name, "PruneLocalRepo") == 0 ||
           g_strcmp0 (method_name, "EnsureRepo") == 0 ||
           g_strcmp0 (method_name, "RunTriggers") == 0 ||
           g_strcmp0 (method_name, "GetRevokefsFd") == 0 ||
           g_strcmp0 (method_name, "CancelPull") == 0)
    {
      guint32 flags;

      action = "org.freedesktop.Flatpak.modify-repo";

      /* all of these methods have flags as first argument, and 1 << 0 as 'no-interaction' */
      g_variant_get_child (parameters, 0, "u", &flags);
      no_interaction = (flags & (1 << 0)) != 0;
    }
  else if (g_strcmp0 (method_name, "UpdateSummary") == 0 ||
           g_strcmp0 (method_name, "GenerateOciSummary") == 0)
    {
      guint32 flags;
      action = "org.freedesktop.Flatpak.metadata-update";

      /* all of these methods have flags as first argument, and 1 << 0 as 'no-interaction' */
      g_variant_get_child (parameters, 0, "u", &flags);
      no_interaction = (flags & (1 << 0)) != 0;
    }

  if (action)
    {
      g_autoptr(AutoPolkitAuthorizationResult) result = NULL;
      g_autoptr(GError) error = NULL;
      PolkitCheckAuthorizationFlags auth_flags;

      if (no_interaction)
        auth_flags = POLKIT_CHECK_AUTHORIZATION_FLAGS_NONE;
      else
        auth_flags = POLKIT_CHECK_AUTHORIZATION_FLAGS_ALLOW_USER_INTERACTION;

      result = polkit_authority_check_authorization_sync (authority, subject,
                                                          action, details,
                                                          auth_flags,
                                                          NULL, &error);
      if (result == NULL)
        {
          g_dbus_error_strip_remote_error (error);
          g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, G_DBUS_ERROR_FAILED,
                                                 "Authorization error: %s", error->message);
          return FALSE;
        }

      authorized = polkit_authorization_result_get_is_authorized (result);
    }

  if (!authorized)
    {
      g_dbus_method_invocation_return_error (invocation,
                                             G_DBUS_ERROR,
                                             G_DBUS_ERROR_ACCESS_DENIED,
                                             "Flatpak system operation %s not allowed for user", method_name);
    }

  return authorized;
}

static void
on_bus_acquired (GDBusConnection *connection,
                 const gchar     *name,
                 gpointer         user_data)
{
  GError *error = NULL;

  g_info ("Bus acquired, creating skeleton");

  g_dbus_connection_set_exit_on_close (connection, FALSE);

  helper = flatpak_system_helper_skeleton_new ();

  flatpak_system_helper_set_version (FLATPAK_SYSTEM_HELPER (helper), 2);

  g_object_set_data_full (G_OBJECT (helper), "track-alive", GINT_TO_POINTER (42), skeleton_died_cb);

  g_dbus_interface_skeleton_set_flags (G_DBUS_INTERFACE_SKELETON (helper),
                                       G_DBUS_INTERFACE_SKELETON_FLAGS_HANDLE_METHOD_INVOCATIONS_IN_THREAD);

  g_signal_connect (helper, "handle-deploy", G_CALLBACK (handle_deploy), NULL);
  g_signal_connect (helper, "handle-deploy-appstream", G_CALLBACK (handle_deploy_appstream), NULL);
  g_signal_connect (helper, "handle-uninstall", G_CALLBACK (handle_uninstall), NULL);
  g_signal_connect (helper, "handle-install-bundle", G_CALLBACK (handle_install_bundle), NULL);
  g_signal_connect (helper, "handle-configure-remote", G_CALLBACK (handle_configure_remote), NULL);
  g_signal_connect (helper, "handle-configure", G_CALLBACK (handle_configure), NULL);
  g_signal_connect (helper, "handle-update-remote", G_CALLBACK (handle_update_remote), NULL);
  g_signal_connect (helper, "handle-remove-local-ref", G_CALLBACK (handle_remove_local_ref), NULL);
  g_signal_connect (helper, "handle-prune-local-repo", G_CALLBACK (handle_prune_local_repo), NULL);
  g_signal_connect (helper, "handle-ensure-repo", G_CALLBACK (handle_ensure_repo), NULL);
  g_signal_connect (helper, "handle-run-triggers", G_CALLBACK (handle_run_triggers), NULL);
  g_signal_connect (helper, "handle-update-summary", G_CALLBACK (handle_update_summary), NULL);
  g_signal_connect (helper, "handle-generate-oci-summary", G_CALLBACK (handle_generate_oci_summary), NULL);
  g_signal_connect (helper, "handle-get-revokefs-fd", G_CALLBACK (handle_get_revokefs_fd), NULL);
  g_signal_connect (helper, "handle-cancel-pull", G_CALLBACK (handle_cancel_pull), NULL);

  g_signal_connect (helper, "g-authorize-method",
                    G_CALLBACK (flatpak_authorize_method_handler),
                    NULL);

  if (!g_dbus_interface_skeleton_export (G_DBUS_INTERFACE_SKELETON (helper),
                                         connection,
                                         FLATPAK_SYSTEM_HELPER_PATH,
                                         &error))
    {
      g_warning ("error: %s", error->message);
      g_error_free (error);
    }
}

static void
on_name_acquired (GDBusConnection *connection,
                  const gchar     *name,
                  gpointer         user_data)
{
  g_info ("Name acquired");
}

static void
on_name_lost (GDBusConnection *connection,
              const gchar     *name,
              gpointer         user_data)
{
  g_info ("Name lost");
  unref_skeleton_in_timeout ();
}

static void
binary_file_changed_cb (GFileMonitor     *file_monitor,
                        GFile            *file,
                        GFile            *other_file,
                        GFileMonitorEvent event_type,
                        gpointer          data)
{
  static gboolean got_it = FALSE;

  if (!got_it)
    {
      g_info ("binary file changed");
      unref_skeleton_in_timeout ();
    }

  got_it = TRUE;
}

static void
message_handler (const gchar   *log_domain,
                 GLogLevelFlags log_level,
                 const gchar   *message,
                 gpointer       user_data)
{
  /* Make this look like normal console output */
  if (log_level & (G_LOG_LEVEL_DEBUG | G_LOG_LEVEL_INFO))
    g_printerr ("FH: %s\n", message);
  else
    g_printerr ("%s: %s\n", g_get_prgname (), message);
}

static gboolean
opt_verbose_cb (const gchar *option_name,
                const gchar *value,
                gpointer     data,
                GError     **error)
{
  opt_verbose++;
  return TRUE;
}

int
main (int    argc,
      char **argv)
{
  gchar exe_path[PATH_MAX + 1];
  ssize_t exe_path_len;
  gboolean replace;
  gboolean show_version;
  GBusNameOwnerFlags flags;
  g_autoptr(GOptionContext) context = NULL;
  g_autoptr(GError) error = NULL;
  const GOptionEntry options[] = {
    { "replace", 'r', 0, G_OPTION_ARG_NONE, &replace,  "Replace old daemon.", NULL },
    { "verbose", 'v', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, &opt_verbose_cb, "Show debug information, -vv for more detail", NULL },
    { "ostree-verbose", 0, 0, G_OPTION_ARG_NONE, &opt_ostree_verbose,"Show OSTree debug information", NULL },
    { "session", 0, 0, G_OPTION_ARG_NONE, &on_session_bus,  "Run in session, not system scope (for tests).", NULL },
    { "no-idle-exit", 0, 0, G_OPTION_ARG_NONE, &no_idle_exit,  "Don't exit when idle.", NULL },
    { "version", 0, 0, G_OPTION_ARG_NONE, &show_version, "Show program version.", NULL},
    { NULL }
  };

  /* The child repo shared between the client process and the
     system-helper really needs to support creating files that
     are readable by others, so override the umask to 022
     Ideally this should be set when needed, but umask is thread-unsafe
     so there is really no local way to fix this.
  */
  umask(022);

  setlocale (LC_ALL, "");

  g_setenv ("GIO_USE_VFS", "local", TRUE);

  if (g_getenv ("FLATPAK_DISABLE_REVOKEFS"))
    disable_revokefs = TRUE;

  g_set_prgname (argv[0]);

  g_log_set_handler (G_LOG_DOMAIN, G_LOG_LEVEL_MESSAGE, message_handler, NULL);

  context = g_option_context_new ("");

  replace = FALSE;
  show_version = FALSE;

  g_option_context_set_summary (context, "Flatpak system helper");
  g_option_context_add_main_entries (context, options, GETTEXT_PACKAGE);

  if (!g_option_context_parse (context, &argc, &argv, &error))
    {
      g_printerr ("%s: %s", g_get_application_name (), error->message);
      g_printerr ("\n");
      g_printerr ("Try \"%s --help\" for more information.",
                  g_get_prgname ());
      g_printerr ("\n");
      g_option_context_free (context);
      return 1;
    }

  if (show_version)
    {
      g_print (PACKAGE_STRING "\n");
      return 0;
    }

  if (opt_verbose > 0)
    g_log_set_handler (G_LOG_DOMAIN, G_LOG_LEVEL_INFO, message_handler, NULL);
  if (opt_verbose > 1)
    g_log_set_handler (G_LOG_DOMAIN, G_LOG_LEVEL_DEBUG, message_handler, NULL);

  if (opt_ostree_verbose)
    g_log_set_handler ("OSTree", G_LOG_LEVEL_DEBUG | G_LOG_LEVEL_INFO, message_handler, NULL);

  if (!on_session_bus)
    {
      authority = polkit_authority_get_sync (NULL, &error);
      if (authority == NULL)
        {
          g_printerr ("Can't get polkit authority: %s\n", error->message);
          return 1;
        }
    }

  exe_path_len = readlink ("/proc/self/exe", exe_path, sizeof (exe_path) - 1);
  if (exe_path_len > 0)
    {
      exe_path[exe_path_len] = 0;
      GFileMonitor *monitor;
      g_autoptr(GFile) exe = NULL;
      g_autoptr(GError) local_error = NULL;

      exe = g_file_new_for_path (exe_path);
      monitor =  g_file_monitor_file (exe,
                                      G_FILE_MONITOR_NONE,
                                      NULL,
                                      &local_error);
      if (monitor == NULL)
        g_warning ("Failed to set watch on %s: %s", exe_path, error->message);
      else
        g_signal_connect (monitor, "changed",
                          G_CALLBACK (binary_file_changed_cb), NULL);
    }

  flags = G_BUS_NAME_OWNER_FLAGS_ALLOW_REPLACEMENT;
  if (replace)
    flags |= G_BUS_NAME_OWNER_FLAGS_REPLACE;

  name_owner_id = g_bus_own_name (on_session_bus ? G_BUS_TYPE_SESSION  : G_BUS_TYPE_SYSTEM,
                                  FLATPAK_SYSTEM_HELPER_BUS_NAME,
                                  flags,
                                  on_bus_acquired,
                                  on_name_acquired,
                                  on_name_lost,
                                  NULL,
                                  NULL);

  cache_dirs_in_use = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);

  /* Ensure we don't idle exit */
  schedule_idle_callback ();

  main_loop = g_main_loop_new (NULL, FALSE);
  g_main_loop_run (main_loop);

  G_LOCK (cache_dirs_in_use);
  g_clear_pointer (&cache_dirs_in_use, g_hash_table_destroy);
  G_UNLOCK (cache_dirs_in_use);

  return 0;
}
